iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
IT Operation

解構作業系統:30 天從 Process、Concurrency 到 Virtual Memory系列 第 8

Day 8|Race Condition:為什麼 counter++ 都能出 Bug?

  • 分享至 

  • xImage
  •  

上一篇談 Thread 時,我們最後留下了一個問題,假設同一個 Process 裡有兩條 Thread,而且它們共享:
int counter = 0;
現在兩條 Thread 都執行一次:counter++;照理來說:
初始 counter = 0
Thread A → +1
Thread B → +1

最後 counter = 2

但在 Multi-threaded Program 裡,最後的結果不一定是 2。
甚至更麻煩的是:有時候程式跑一百次都正常,第 101 次才突然出錯。
問題是:

Race Condition


1. 一行 counter++,CPU 真的只做一件事嗎?

counter++:
counter

│ +1

counter

一個動作,結束。
但是Source Code 的「一行」,不代表 CPU 執行時一定是唯一個不可分割的操作,假設:
counter++;
counter++;
最後 Memory 裡:counter = 1,為什麼不是 2?
因為兩條 Thread 都在對方寫回之前讀到了:counter = 0,因此它們各自都算出:0 + 1 = 1
最後兩邊都把 1 寫回去。
其中一次更新就像被吃掉了。
這種情況常被稱為:Lost Update
也就是某次更新因為並行操作互相覆蓋,最後沒有反映在結果裡。

原本在程式邏輯上希望被當成一個完整操作的事情,實際上可能由多個步驟組成,而其他 Thread 有機會在這些步驟之間介入。


2. 所以 Race Condition 到底是什麼?

假設:
Thread A → read x
Thread B → read x
兩個 Thread 都只是讀取,而且沒有其他修改,通常不會因為「兩個人一起讀」就產生我們這裡說的資料競爭問題。會出問題的是:

  • 有共享狀態
  • 存在可能衝突的操作,常見情況是至少一方會修改
  • 缺乏適當的同步
  • 程式結果因此受到執行時序影響

Race Condition:程式的正確結果依賴多個執行流程發生的相對順序或時機,而這個順序沒有被程式正確控制。
(2)同一份程式 + 同一份輸入 + 不同 Execution Interleaving = 不同結果
例如同一段程式:counter++;可能有一種執行順序:
Thread A:Load 0 | Thread B:Load 1
Thread A:Add 1 | Thread B:Add 1
Thread A:Store 1| Thread B:Store 2
結果:
counter = 2

但是也可能:
Thread A:Load 0
Thread B:Load 0
Thread A:Add 1
Thread B:Add 1
Thread A:Store 1
Thread B:Store 1
結果:
counter = 1
這就是不同 Execution Interleaving 有 不同結果。

(3)Interleaving 是什麼?:不同 Thread 的操作彼此交錯執行。
只要各 Thread 自己需要遵守的執行關係沒有被破壞,不同 Thread 之間就可能出現許多不同的 interleaving。
所以Concurrency 最難處理的地方之一,就是:
不能只看某一條 Thread 自己的程式碼,還必須考慮它和其他 Thread 交錯之後會發生什麼。


3. Race Condition 一定要有兩顆 CPU 同時執行才會發生嗎?

不需要。
即使只有One CPU Core,也可能發生 Race Condition。
假設 Thread A 正在執行:
Load counter → 0
Add 1 → 1
但還沒 Store 回去,這時 OS 因為排程等原因讓 CPU 改去執行 Thread B。
所以,多個 execution flows 的操作可以產生不受控制的 interleaving,而程式的正確性依賴這些操作的相對順序。
因此,在 Multi-core CPU 上,問題甚至可能更複雜,因為不同 Threads 確實可以在不同 Cores 上平行執行。


4.Critical Section 是什麼?為什麼它是解 Race Condition 的核心?

假設:counter++;這段操作會修改所有 Threads 都能存取的Shared Data
那這一小段程式就不能毫無限制地讓多個 Thread 同時進來操作。
這種會存取共享資源,而且需要避免不安全並行操作的程式區域,通常稱為:Critical Section

也就是說,counter++;成為一個需要被保護的 Critical Section。

Thread A
   │
   ▼
┌─────────────────┐
│ Critical Section│
│                 │
│   counter++;    │
│                 │
└─────────────────┘

如果 Thread A 正在執行這段會修改共享資料的程式碼,那 Thread B 暫時不能同時執行同一個受保護區域。
等 A 完成:
Thread A 離開

Thread B 才進入
這樣就可以讓:
A:Load
A:Add
A:Store
一個受到保護的操作序列完成之後,再輪到 B
(2)而這種:同一時間只允許一個執行流程進入某個 Critical Section叫做:Mutual Exclusion互斥。(Mutex,名字其實就是從Mutual Exclusion來的)


5.為什麼 Race Condition 特別難抓?

Race Condition 有一個非常討厭的特性:它不一定每次發生。假設我們寫:
Thread A:counter++
Thread B:counter++

第一次執行:
A 完整跑完

B 完整跑完 ; 結果 = 2
第二次:一樣完美;
第三次:
A Load

B Load

A Store

B Store ; 結果 = 1
這就是 Concurrent Bug 很難 Debug 的原因之一,因為它可能受到:

  • Scheduler 的排程時機
  • CPU Core 的執行情況
  • 系統當下負載
  • Thread 的相對執行速度
  • I/O Timing
  • Compiler / CPU 執行與 Memory Model 相關行為等因素影響。
    所以:
    「跑了很多次都沒出錯」並不能證明程式沒有 Race Condition。

6.那到底要怎麼解?

Thread A / Thread B 兩邊可以任意進入:Load / Add / Store
所以發生 Race Condition。

那我們改成:

Thread A
   │
   │ 取得進入權
   ▼
┌──────────────────┐
│ Critical Section │
│                  │
│   counter++;     │
│                  │
└──────────────────┘
   │
   │ 離開
   ▼
Thread B 才能進入

也就是要建立:Synchronization
建立多個 execution flows 之間需要遵守的協調規則,避免不安全的執行順序。其中一種最經典的工具就是:Mutex,但 Mutex 並不是唯一的方法,後面還會依據情境有不同的解法。


今天的結論

Shared State
+
Concurrent Access
+
缺乏正確 Synchronization

執行結果受到 Timing / Interleaving 影響

Race Condition
解決方法之一是:我們把那些需要避免不安全 Concurrent Access 的程式區域稱為:

Critical Section

(而這種同一時間只允許一個執行流程進入某個 Critical Section叫做:Mutual Exclusion)

接下來要解決的問題就是:
如果我們希望某個操作「要嘛完整發生,要嘛完全沒發生」,中間不能被其他 Thread 看到半套狀態,CPU 和 OS 到底要怎麼做到?

下一篇:

Day 9|Atomicity:什麼叫做「不可分割」?


上一篇
Day 7|Thread 到底共享什麼、不共享什麼?
下一篇
Day 9|Atomicity:?
系列文
解構作業系統:30 天從 Process、Concurrency 到 Virtual Memory9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言